"""Codex审查器核心逻辑：监控codex进程并检查终止条件。"""

import time
import asyncio
import subprocess
import sys
import os
import logging
from pathlib import Path
from typing import Dict, Any

# 配置logging输出到stderr（不影响MCP的stdout JSON通信）
logging.basicConfig(
    level=logging.INFO,
    format='[%(levelname)s] %(message)s',
    stream=sys.stderr
)
logger = logging.getLogger(__name__)

# 导入GUI工具和编码工具
try:
    from .gui_utils import check_gui_available
    from .encoding_utils import EncodingDetector
    from .data_models import ReviewResult
except ImportError:
    from gui_utils import check_gui_available
    from encoding_utils import EncodingDetector
    from data_models import ReviewResult


class CodexReviewer:
    """监控Codex审查进程，检查终止条件，返回审查结果"""

    # Timeout constants (seconds)
    CODEX_VERSION_CHECK_TIMEOUT = 30
    UI_STARTUP_CHECK_DELAY = 0.5
    PROCESS_TERMINATION_TIMEOUT = 3
    UI_TERMINATION_TIMEOUT = 2
    REPORT_DETECTION_WAIT_TIME = 10
    MAIN_LOOP_POLL_INTERVAL = 1
    LOG_TASK_CLEANUP_TIMEOUT = 5

    # File I/O constants
    LOG_FILE_LINE_BUFFERING = 1
    REPORT_MIN_SIZE_BYTES = 100

    def __init__(self, timeout_seconds: int = 1800):
        """初始化审查器

        Args:
            timeout_seconds: 超时时间（秒），默认1800秒（30分钟）
        """
        self.timeout_seconds = timeout_seconds

    def _check_codex_installed(self) -> tuple[bool, str]:
        """检查Codex CLI是否已安装

        Returns:
            (is_installed, version_or_error): True表示已安装，False表示未安装或检查失败
        """
        try:
            result = subprocess.run(
                'codex --version',
                capture_output=True,
                text=True,
                timeout=30,  # Windows npm脚本需要更长时间
                shell=True  # Windows需要shell执行.cmd文件
            )
            if result.returncode == 0:
                version = result.stdout.strip()
                return True, version
            else:
                return False, f"Codex command failed: {result.stderr}"
        except subprocess.TimeoutExpired:
            return False, "Codex command timed out after 30 seconds"
        except FileNotFoundError:
            return False, "Codex CLI not found. Please install from: https://github.com/openai/codex-cli"
        except Exception as e:
            return False, f"Error checking Codex: {str(e)}"

    async def _cleanup_process(self, process, timeout=5):
        """优雅地清理进程及其子进程

        尝试terminate()优雅终止，等待timeout秒，如果失败则使用kill()强制终止。
        即使主进程已退出，也会尝试清理，因为子进程可能成为孤儿进程继续运行。

        Args:
            process: asyncio subprocess对象
            timeout: terminate后等待的秒数
        """
        if process is None:
            return  # 进程对象不存在

        # 注意：不检查returncode，即使主进程已退出也尝试terminate/kill
        # 这是因为在Windows上，shell启动的子进程可能成为孤儿进程继续运行

        try:
            # 尝试优雅终止
            process.terminate()
            await asyncio.wait_for(process.wait(), timeout=timeout)
            logger.info(f"[MCP] Process {process.pid} terminated gracefully")
        except asyncio.TimeoutError:
            # terminate失败，强制kill
            logger.warning(f"[MCP] Process {process.pid} did not terminate, killing")
            process.kill()
            try:
                await asyncio.wait_for(process.wait(), timeout=2)
                logger.info(f"[MCP] Process {process.pid} killed")
            except asyncio.TimeoutError:
                logger.error(f"[MCP] Failed to kill process {process.pid}")
        except ProcessLookupError:
            # 进程已不存在（可能已自然退出）
            logger.info(f"[MCP] Process {process.pid} already exited")

    async def _capture_and_write_log(self, stdout: asyncio.StreamReader, log_path: Path):
        """Capture Codex stdout in real-time, detect encoding, write to UTF-8 log.

        This replaces shell redirection (>) to achieve 100% control over codex.log encoding.
        Each line is decoded using smart encoding detection, then written as UTF-8.

        Args:
            stdout: asyncio.StreamReader from subprocess
            log_path: Path to codex.log file
        """
        try:
            with open(log_path, 'w', encoding='utf-8', buffering=1) as f:
                while True:
                    # Read line as bytes
                    line_bytes = await stdout.readline()
                    if not line_bytes:
                        break  # EOF

                    # Smart decode (try UTF-8/GBK/GB18030)
                    text = EncodingDetector.decode_bytes(line_bytes)

                    # Write to UTF-8 log (no BOM)
                    f.write(text)
        except Exception as e:
            # Log capture failure should not crash the review workflow
            logger.error(f"[MCP] Log capture error: {str(e)}")

    async def start_review(
        self,
        session_dir: str,
        project_root: str
    ) -> ReviewResult:
        """启动Codex审查并监控进度（异步版本，实时日志流式处理）

        Args:
            session_dir: Session目录路径
            project_root: 项目根目录路径（Codex工作目录）

        Returns:
            ReviewResult instance with review data
        """
        session_path = Path(session_dir)
        report_path = session_path / "report.md"
        log_path = session_path / "codex.log"

        start_time = time.time()

        # Check if Codex is installed
        is_installed, version_or_error = self._check_codex_installed()
        if not is_installed:
            # 生成错误报告
            error_report = f"""# Review Report

## Status
error

## Error Message
{version_or_error}

## Summary
Codex CLI is not available. Please install it from: https://github.com/openai/codex-cli
"""
            report_path.write_text(error_report, encoding='utf-8')

            return ReviewResult(
                status="failed",
                report_content=error_report,
                log_tail=version_or_error,
                execution_time=0,
                session_dir=None  # Codex未安装，session未创建
            )

        try:
            # 构建环境变量（继承当前环境并强制UTF-8）
            env = os.environ.copy()
            env['PYTHONIOENCODING'] = 'utf-8'  # 强制Python I/O使用UTF-8
            env['PYTHONUTF8'] = '1'  # Python 3.7+ UTF-8模式

            # 计算session目录相对于项目根的相对路径
            project_root_path = Path(project_root)
            session_rel_path = session_path.relative_to(project_root_path)

            # 构建Codex命令，明确指定session文件的相对路径
            # Codex工作目录设为项目根，这样可以访问所有项目文件
            codex_cmd = (
                f'codex exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox '
                f'"Please read {session_rel_path}/review-request.md and complete the review task. '
                f'Write report.md to {session_rel_path}/"'
            )

            # 启动异步子进程（捕获 stdout/stderr 以实现编码控制）
            # cwd设为项目根目录，让Codex能访问所有项目文件
            process = await asyncio.create_subprocess_shell(
                codex_cmd,
                cwd=str(project_root_path),
                env=env,  # 传入环境变量
                stdout=asyncio.subprocess.PIPE,  # 捕获标准输出
                stderr=asyncio.subprocess.STDOUT  # 合并标准错误到标准输出
            )

            # === 启动日志捕获任务（后台异步运行）===
            # 实时读取 Codex stdout，智能检测编码，写入 UTF-8 日志
            log_task = asyncio.create_task(
                self._capture_and_write_log(process.stdout, log_path)
            )
            logger.info(f"[MCP] Log capture task started, writing to {log_path}")

            # === 启动Codex进程后，检查并启动监控UI ===
            ui_process = None

            # 检查GUI环境是否可用
            logger.info("[MCP] Checking GUI availability...")
            gui_available = check_gui_available()
            logger.info(f"[MCP] GUI available: {gui_available}")

            if gui_available:
                try:
                    # 构建UI脚本路径
                    script_dir = Path(__file__).parent
                    ui_script = script_dir / "codex_monitor_ui.py"

                    logger.info(f"[MCP] UI script path: {ui_script}")
                    logger.info(f"[MCP] UI script exists: {ui_script.exists()}")

                    if not ui_script.exists():
                        logger.warning(f"[MCP] UI script not found at {ui_script}, skipping GUI")
                    else:
                        logger.info(f"[MCP] Launching GUI with Python: {sys.executable}")
                        logger.info(f"[MCP] Log path: {log_path}")

                        # 启动UI子进程
                        ui_process = await asyncio.create_subprocess_exec(
                            sys.executable,  # Python解释器路径
                            "-u",  # 无缓冲输出
                            str(ui_script),
                            "--log-path", str(log_path),
                            "--review-request-path", str(session_path / "review-request.md"),
                            "--draft-path", str(session_path / "draft.md"),
                            stdin=asyncio.subprocess.DEVNULL,  # 避免继承父进程stdin
                            stdout=asyncio.subprocess.PIPE,  # 捕获输出用于调试
                            stderr=asyncio.subprocess.PIPE
                        )

                        # 等待一小段时间，检查UI是否成功启动
                        await asyncio.sleep(0.5)

                        if ui_process.returncode is not None:
                            # UI进程已经退出（启动失败）
                            stdout, stderr = await ui_process.communicate()
                            logger.error(f"[MCP] GUI failed to start (exit code: {ui_process.returncode})")
                            logger.error(f"[MCP] GUI stdout: {stdout.decode('utf-8', errors='replace')}")
                            logger.error(f"[MCP] GUI stderr: {stderr.decode('utf-8', errors='replace')}")
                            ui_process = None
                        else:
                            logger.info(f"[MCP] GUI started successfully (PID: {ui_process.pid})")

                except Exception as e:
                    # UI启动失败不影响主流程，继续执行
                    logger.error(f"[MCP] Exception while launching GUI: {type(e).__name__}: {str(e)}")
                    import traceback
                    logger.error(f"[MCP] Traceback: {traceback.format_exc()}")
            else:
                logger.info("[MCP] GUI not available, running in headless mode")

            # 主监控循环：等待进程退出或超时/UI中止
            start_time = time.time()
            report_detected_time = None  # 首次检测到report.md的时间戳

            try:
                while True:
                    # ========================================
                    # 检查1：UI进程状态（最高优先级）
                    # ========================================
                    if ui_process and ui_process.returncode is not None:
                        # UI进程已退出
                        if ui_process.returncode == 99:
                            # 退出码99：用户主动中止审查
                            # 终止codex进程
                            if process.returncode is None:
                                await self._cleanup_process(process, timeout=2)

                            # 生成中止报告
                            self._generate_error_report(
                                report_path,
                                status="error",
                                error_message="User aborted the review",
                                summary="The review was manually cancelled by the user through the monitor UI."
                            )

                            return ReviewResult(
                                status="failed",
                                report_content=self._read_report(report_path),
                                log_tail=self._read_log_tail(log_path, 10),
                                execution_time=int(time.time() - start_time),
                                session_dir=session_dir
                            )
                        else:
                            # 其他退出码：UI意外崩溃
                            # 不中止审查，继续等待codex完成
                            ui_process = None  # 清空引用，不再检查

                    # ========================================
                    # 检查2：超时
                    # ========================================
                    elapsed = time.time() - start_time
                    if elapsed > self.timeout_seconds:
                        # 超时，终止进程
                        await self._cleanup_process(process, timeout=2)
                        if ui_process and ui_process.returncode is None:
                            ui_process.kill()

                        await process.wait()

                        # 生成超时报告
                        self._generate_error_report(
                            report_path,
                            status="timeout",
                            error_message=f"Review process exceeded timeout limit ({self.timeout_seconds} seconds)",
                            summary="The Codex review was terminated due to timeout. This may indicate complex review requirements, Codex performance issues, or the need to increase timeout_seconds parameter."
                        )

                        return ReviewResult(
                            status="timeout",
                            report_content=self._read_report(report_path),
                            log_tail=self._read_log_tail(log_path, 10),
                            execution_time=int(elapsed),
                            session_dir=session_dir
                        )

                    # ========================================
                    # 检查3：report.md保底方案（10秒强制终止）
                    # ========================================
                    # 如果report.md已生成且Codex进程未退出，等待10秒后强制终止
                    # 要求文件至少100字节（避免误判空文件）
                    if report_path.exists() and report_path.stat().st_size > 100:
                        if report_detected_time is None:
                            # 首次检测到report.md
                            report_detected_time = time.time()
                            logger.info(f"[MCP] report.md detected ({report_path.stat().st_size} bytes), starting 10-second countdown")
                        else:
                            # 检查是否已等待超过10秒
                            report_wait_time = time.time() - report_detected_time
                            if report_wait_time > 10:
                                # 超过10秒，先关闭UI（让用户有足够时间查看日志）
                                logger.info("[MCP] 10 seconds elapsed since report.md detected, closing UI")
                                if ui_process and ui_process.returncode is None:
                                    ui_process.kill()
                                    try:
                                        await asyncio.wait_for(ui_process.wait(), timeout=2)
                                    except asyncio.TimeoutError:
                                        pass

                                # 然后检测codex进程是否还在运行
                                if process.returncode is None:
                                    logger.info("[MCP] Codex process still running, force-terminating")
                                    await self._cleanup_process(process, timeout=2)
                                else:
                                    logger.info("[MCP] Codex process already exited")

                                # 跳出循环，进入正常结果读取流程
                                break

                    # ========================================
                    # 检查4：等待进程退出（1秒超时）
                    # ========================================
                    try:
                        await asyncio.wait_for(process.wait(), timeout=1)
                        # 进程已退出，跳出循环
                        break
                    except asyncio.TimeoutError:
                        # 进程还在运行，继续循环
                        continue

            finally:
                # 清理codex进程（确保所有退出路径都清理进程）
                await self._cleanup_process(process, timeout=3)

                # 清理UI进程
                if ui_process and ui_process.returncode is None:
                    ui_process.kill()
                    try:
                        await asyncio.wait_for(ui_process.wait(), timeout=2)
                    except asyncio.TimeoutError:
                        pass

                # 等待日志捕获任务完成（读取剩余的 stdout）
                try:
                    await asyncio.wait_for(log_task, timeout=5)
                    logger.info("[MCP] Log capture task completed")
                except asyncio.TimeoutError:
                    logger.warning("[MCP] Log capture task timeout, cancelling")
                    log_task.cancel()
                    try:
                        await log_task
                    except asyncio.CancelledError:
                        pass
                except Exception as e:
                    logger.error(f"[MCP] Log capture task error: {str(e)}")

            # 进程已退出，读取结果
            # 检查report.md是否存在
            if not report_path.exists() or report_path.stat().st_size == 0:
                self._generate_error_report(
                    report_path,
                    status="error",
                    error_message=f"Codex process exited without generating report (exit code {process.returncode})",
                    summary="The Codex review process terminated unexpectedly. Check codex.log for details."
                )

            return ReviewResult(
                status="completed" if process.returncode == 0 else "failed",
                report_content=self._read_report(report_path),
                log_tail=self._read_log_tail(log_path, 10),
                execution_time=int(time.time() - start_time),
                session_dir=session_dir
            )

        except Exception as e:

            # 生成异常报告
            exception_report = f"""# Review Report

## Status
error

## Error Message
{str(e)}

## Summary
An unexpected error occurred during the review process.
"""
            report_path.write_text(exception_report, encoding='utf-8')

            return ReviewResult(
                status="failed",
                report_content=exception_report,
                log_tail=f"Error: {str(e)}",
                execution_time=int(time.time() - start_time),
                session_dir=session_dir
            )

    def _get_log_line_count(self, log_path: Path) -> int:
        """Get log file line count"""
        if not log_path.exists():
            return 0
        try:
            content = EncodingDetector.read_file(log_path)
            return content.count('\n') + 1
        except:
            return 0

    def _read_log_tail(self, log_path: Path, lines: int) -> str:
        """Read last N lines of log file"""
        if not log_path.exists():
            return ""
        try:
            content = EncodingDetector.read_file(log_path)
            all_lines = content.splitlines()
            return "\n".join(all_lines[-lines:])
        except:
            return ""

    def _read_report(self, report_path: Path) -> str:
        """Read report.md content"""
        if not report_path.exists():
            return ""
        try:
            return EncodingDetector.read_file(report_path)
        except UnicodeDecodeError as e:
            return f"[ENCODING ERROR] Cannot read report.md: {str(e)}"

    def _generate_error_report(
        self,
        report_path: Path,
        status: str,
        error_message: str,
        summary: str
    ):
        """生成并写入错误报告

        Args:
            report_path: report.md文件路径
            status: 状态（error/timeout等）
            error_message: 错误消息
            summary: 摘要说明
        """
        content = f"""# Review Report

## Status
{status}

## Error Message
{error_message}

## Summary
{summary}
"""
        report_path.write_text(content, encoding='utf-8')

    async def _terminate_process(
        self,
        process,
        timeout: int = None,
        process_name: str = "process"
    ):
        """优雅终止异步进程

        Args:
            process: 异步进程对象
            timeout: 等待超时（秒），默认使用PROCESS_TERMINATION_TIMEOUT
            process_name: 进程名称（用于日志）
        """
        if timeout is None:
            timeout = self.PROCESS_TERMINATION_TIMEOUT

        if process and process.returncode is None:
            logger.info(f"[MCP] Terminating {process_name}...")
            process.kill()
            try:
                await asyncio.wait_for(process.wait(), timeout=timeout)
            except asyncio.TimeoutError:
                logger.warning(f"[MCP] {process_name} termination timed out")
